Skip to content

feat: virtual Amiibo scanning and the game's Amiibo rewards - #1521

Draft
nicholasgower wants to merge 7 commits into
NotRexed:mainfrom
nicholasgower:feat/amiibo
Draft

nicholasgower wants to merge 7 commits into
NotRexed:mainfrom
nicholasgower:feat/amiibo

Conversation

@nicholasgower

@nicholasgower nicholasgower commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

The game's amiibo function asks Horizon for an NFC tag, but a PC has no reader, so every scan currently falls through to the generic stubs. This adds a virtual tag, so the game sees a real Amiibo ID and runs its own reward code.

What changed

  • host/nn_nfp.c answers nn::nfp and nn::nfp::sdk at the SDK boundary: device listing, detection, mount/unmount, tag/model/common/register info, application areas, events and amiibo settings.
  • host/nfp_amiibo.h holds the SDK layouts and the selectable figures: Link (Ocarina of Time, the default), Zelda, Young Link, Young Zelda, the two 2026 Ocarina of Time figures (Mask 101/102), and Mario for the unsupported path.
  • The Insert menu's Gameplay page has an Amiibo row, whose last entry, None, presents no virtual tag (so the game waits for a real reader, as it does today). HOS_AMIIBO=<profile or 0x...>, HOS_AMIIBO_ID and HOS_AMIIBO_RANDOM_UID=1 override it; amiibo= is the saved key.
  • The tag arrives a moment after StartDetection rather than inside it, as a figure being tapped does. Presenting it inside the call made the whole mount-and-read finish before the game had drawn its "touch the amiibo" prompt.
  • StopDetection puts the reader back to Initialized, so the game's next scan starts detection again instead of waiting on a state it never leaves.
  • GetApplicationArea/SetApplicationArea are answered under both SDK spellings. The game imports the unsigned long (Pm/Em) form, and the mangled name differs from unsigned long long (Py/Ey), so those calls were reaching the stub and no size was written.
  • tests/test_amiibo.py checks the SDK offsets, the ids, the numbering endianness, the UIDs and the effects.

Testing

  • python -I -B tests/test_amiibo.py passes, and python -I -B tests/run_all.py passes apart from two unrelated environment-only checks (Windows symlink privileges in nx2/test_nup_paths.py, and windows.h not on the test's include path in tests/test_precompile_compute_guard.py).
  • The Amiibo effect has not been confirmed by a human. It is tested only in this checkout, with scripted input against a local copy of the game.
  • What the game does now: pressing the amiibo button opens its "Use amiibo — Touch the amiibo to the NFC touchpoint" prompt, and the game asks for the reader, starts detection, sees the tag appear, mounts it, reads GetTagInfo (UID, NFC-A/TYPE2) and GetModelInfo (e.g. character 010000, series 09, numbering 034b for the Ocarina of Time Link figure), then stops detection. So the game receives the right tag and the right figure.
  • What it does not do yet: the prompt stays open and the game keeps re-scanning (detect, mount, read the model, stop, start again) instead of giving the reward. Pressing A or B closes the prompt and play continues normally, with no item, mask or "this amiibo does not work" message seen. The remaining gap is in the game's own amiibo screen, not in the tag data the emulator returns.
  • The 2026 figures' retail IDs are not public yet. Their profiles use the series' next numbering; a real figure's id can be presented exactly with HOS_AMIIBO=0x....

No game files or build output are included.

@nicholasgower
nicholasgower marked this pull request as draft November 10, 2026 07:36
@nicholasgower

Copy link
Copy Markdown
Contributor Author

Debugging and testing is ongoing. Once I've confirmed this functionality works, I will rebase and declare it ready for review.

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged onto current main (15646d1), this is clean, and tests/run_all.py passes there (377 passed, 0 failed). The CI failures come from the old base: nx2/test_draw_parameters.py's mesh_fallback fixture, and the Windows nx2/test_nup_out_of_space.py path check, which fail the same way on other PRs from that period. A few points for when it comes out of draft:

  • host/nn_nfp.c:150 reads HOS_AMIIBO_RANDOM_UID with atoi. Since #2042, switches use env_on from hookshot_env.h, so that =true and =on work.
  • The random UID comes from rand(), and nothing in host/ calls srand, so every launch produces the same sequence of UIDs. That works against the once-a-day rule this switch is for: the first scan after a restart repeats a UID the game has already seen. Seeding once from hos_now_ns() would fix it.
  • The default figure is Link (index 0), so every player gets a working amiibo with no reader as soon as this lands. That seems like a maintainer call; defaulting to None would keep today's behaviour.

nicholasgower and others added 7 commits November 12, 2026 10:50
The game's amiibo function asks Horizon for an NFC tag, but a PC has no reader, so every scan fell through to the generic stubs. This presents a virtual tag instead: the game sees a real Amiibo ID and runs its own reward code.

- host/nn_nfp.c answers nn::nfp and nn::nfp::sdk at the SDK boundary: detection, mount, tag/model/common/register info, application areas and events.
- host/nfp_amiibo.h holds the SDK layouts and the selectable figures: Link (Ocarina of Time, the default), Zelda, Young Link, Young Zelda, the two 2026 Ocarina of Time figures (Mask 101/102), and Mario for the unsupported path.
- The Insert menu's Gameplay page has an Amiibo row; HOS_AMIIBO=<profile or 0x...>, HOS_AMIIBO_ID and HOS_AMIIBO_RANDOM_UID override it.
- tests/test_amiibo.py checks the SDK offsets, IDs, numbering endianness, UIDs and effects.

This has not been tested by a human: no in-game scan, reward, mask or unsupported-figure path has been run. The game-free check passes; the 2026 figures' retail IDs are not public, so their profiles use the series' next numbering and can be replaced with HOS_AMIIBO=0x....

Assisted-by: DeepSeek V4.1 Flash
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
The first human test showed the game's amiibo screen scanning the virtual tag but never applying the effect. Its runtime log had one unimplemented nfp import: nn::nfp::GetNpadId(unsigned int*, DeviceHandle const&) (EPj), because SDK 22's NpadIdType is a uint32_t typedef, not the enum name the older SDK mangled. The generic stub returned without writing the controller ID, so the game could not map the NFC device. Add that overload and its nn::nfp::sdk alias; the older enum name stays for other SDKs.

Assisted-by: DeepSeek V4.1 Flash
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
The first fix still did not apply the reward, so log what the game actually receives: TagInfo, ModelInfo, RegisterInfo and CommonInfo once per distinct value, plus the device state, mount and application-area flow. The log is bounded (it only prints when the returned data changes) so a repeated scan does not flood it.

Assisted-by: DeepSeek V4.1 Flash
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
The read log shows the game mounts the tag and reads the model, then resets detection without reading the register/common data. Log StartDetection, StopDetection, Unmount, GetState and the three event attaches, and signal the activate event on a late attach: the virtual tag is always present, so a UI that attaches after the worker's scan must not miss it.

Assisted-by: DeepSeek V4.1 Flash
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
The whole tag was read inside StartDetection, so the game drew its 'touch the amiibo' prompt after the mount, model read and StopDetection had already finished. The tag now appears 300 ms after StartDetection, as a figure being tapped does, and StopDetection puts the reader back to Initialized so the game's next scan starts detection again.

Also answer the GetApplicationArea/SetApplicationArea overloads the game imports: its SDK spells the size unsigned long (Pm/Em), not unsigned long long (Py/Ey), and the mangled names differ, so those calls reached the stub and got no size written.

The Amiibo menu row gains a None entry (index 7) that presents no virtual tag, matching HOS_AMIIBO=none.
The header promised the reward, which is the game's decision and is not confirmed yet: the virtual tag only has to present the figure's data.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants